iT邦幫忙

2

gorm的Find跟Scan

zyx 2023-11-17 17:11:471042 瀏覽
  • 分享至 

  • xImage
  •  

gorm的Find跟Scan

筆者在使用gorm時通常都習慣用Find,不過最近因為在查找gorm的官網時意外看到Scan的用法,就好奇查了一下他們有什麼不同,最後就整理出了這篇文章

Find和Scan在查詢時gorm做了什麼

Find

可以先看一下Find在查詢資料時做了什麼事情(下面的code可以不用看的太認真,看下面的結論比較重要,除非你真的很想知道非常底層的部分)

func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
	tx = db.getInstance()
	if len(conds) > 0 {
		if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {
			tx.Statement.AddClause(clause.Where{Exprs: exprs})
		}
	}
	tx.Statement.Dest = dest
	return tx.callbacks.Query().Execute(tx)
}

我們特別看下面這段就可以了解到,Find是在db進行Query執行前就進行結構的轉換

	tx.Statement.Dest = dest
	tx.callbacks.Query().Execute(tx)

Scan

再來看看Scan在查詢資料時做了什麼事情(下面的code可以不用看的太認真,看下面的結論比較重要,除非你真的很想知道非常底層的部分)

func (db *DB) Scan(dest interface{}) (tx *DB) {
	config := *db.Config
	currentLogger, newLogger := config.Logger, logger.Recorder.New()
	config.Logger = newLogger

	tx = db.getInstance()
	tx.Config = &config
    
    // 請看這邊~~
	if rows, err := tx.Rows(); err == nil {
		if rows.Next() {
			tx.ScanRows(rows, dest)
		} else {
			tx.RowsAffected = 0
		}
		tx.AddError(rows.Close())
	}

	currentLogger.Trace(tx.Statement.Context, newLogger.BeginAt, func() (string, int64) {
		return newLogger.SQL, tx.RowsAffected
	}, tx.Error)
	tx.Logger = currentLogger
	return
}

// 上面的Scan裡面的tx.Rows() 就會呼叫這邊
func (db *DB) Rows() (*sql.Rows, error) {
	tx := db.getInstance().Set("rows", true)
	tx = tx.callbacks.Row().Execute(tx)
	rows, ok := tx.Statement.Dest.(*sql.Rows)
	if !ok && tx.DryRun && tx.Error == nil {
		tx.Error = ErrDryRunModeUnsupported
	}
	return rows, tx.Error
}

// 上面的Scan裡面的tx.ScanRows(rows, dest) 就會呼叫這邊
func (db *DB) ScanRows(rows *sql.Rows, dest interface{}) error {
	tx := db.getInstance()
	if err := tx.Statement.Parse(dest); !errors.Is(err, schema.ErrUnsupportedDataType) {
		tx.AddError(err)
	}
	tx.Statement.Dest = dest
	tx.Statement.ReflectValue = reflect.ValueOf(dest)
	for tx.Statement.ReflectValue.Kind() == reflect.Ptr {
		elem := tx.Statement.ReflectValue.Elem()
		if !elem.IsValid() {
			elem = reflect.New(tx.Statement.ReflectValue.Type().Elem())
			tx.Statement.ReflectValue.Set(elem)
		}
		tx.Statement.ReflectValue = elem
	}
	Scan(rows, tx, ScanInitialized)
	return tx.Error
}

我們特別看下面這兩段就可以了解到,Scan是在db進行Query執行後才進行結構的轉換

    func (db *DB) Scan(dest interface{}) (tx *DB) {
        ...
        if rows, err := tx.Rows(); err == nil {
            if rows.Next() {
                tx.ScanRows(rows, dest)
            } else {
                tx.RowsAffected = 0
            }
            tx.AddError(rows.Close())
        }
        ...
    }
    func (db *DB) Rows() (*sql.Rows, error) {
        tx := db.getInstance().InstanceSet("rows", true)
        tx.callbacks.Row().Execute(tx)
        rows, ok := tx.Statement.Dest.(*sql.Rows)
        ...
        return rows, tx.Error
    }
    
    func (db *DB) ScanRows(rows *sql.Rows, dest interface{}) error {
        ...
        tx.Statement.Dest = dest
        tx.Statement.ReflectValue = reflect.ValueOf(dest)
        ...
    }

上述的差異會造成什麼影響?

造成的差異為下
1.是Scan會需要透過Row()指定資料表的名稱(是真的在db的資料表名稱,users這種,不能用User)

2.Query可用的方法有差異可以在這個檔案位置看到(gorm/callbacks/callbacks.go)
Find的Query:
queryCallback := db.Callback().Query()
queryCallback.Register("gorm:query", Query)
queryCallback.Register("gorm:preload", Preload)
queryCallback.Register("gorm:after_query", AfterQuery)

Scan的Query:
db.Callback().Row().Register("gorm:row", RowQuery)
db.Callback().Raw().Register("gorm:raw", RawExec)

3.雖然說scan在實行時要寫sql語法比較麻煩,但相對的它指定搜尋特定欄位,也是有它的好處(避免select * 的狀況)

4.Raw只能用在Scan不能用在Find
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result) V
db.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Find(&result) X

結論

雖然Find可以讓我們在寫code的時候透過Preload或是Distinct之類的方法可以很輕鬆的幫我們做關聯查詢,但其實背後也犧牲了資料庫查詢的效能,如果遇到效能瓶頸時,或許把Find改成Row是一個不錯的做法。

以上提供的解法為筆者的淺見。若以上內容有誤,煩請各位讀者用力指正,謝謝。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言